// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chrome/browser/ui/signin/profile_colors_util.h"

#include "base/containers/contains.h"
#include "base/format_macros.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/bind.h"
#include "base/test/task_environment.h"
#include "chrome/browser/new_tab_page/chrome_colors/generated_colors_info.h"
#include "chrome/browser/profiles/profile_attributes_entry.h"
#include "chrome/browser/profiles/profile_attributes_init_params.h"
#include "chrome/browser/profiles/profile_attributes_storage.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "chrome/browser/themes/browser_theme_pack.h"
#include "chrome/browser/themes/custom_theme_supplier.h"
#include "chrome/test/base/testing_browser_process.h"
#include "chrome/test/base/testing_profile_manager.h"
#include "components/account_id/account_id.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/gfx/color_utils.h"
#include "ui/native_theme/native_theme.h"

namespace {

constexpr size_t kColorsCount = base::size(chrome_colors::kGeneratedColorsInfo);

size_t ReturnNth(size_t n, size_t size) {
  DCHECK_LT(n, size);
  return n;
}

size_t CaptureCountAndReturnZero(size_t* storage, size_t count) {
  *storage = count;
  return 0;
}

const chrome_colors::ColorInfo& GetColor(size_t index) {
  return chrome_colors::kGeneratedColorsInfo[index];
}

SkColor GetHighlightColor(size_t index) {
  ProfileThemeColors theme_colors =
      GetProfileThemeColorsForAutogeneratedColor(GetColor(index).color);
  return theme_colors.profile_highlight_color;
}

bool IsColorMatchingColorScheme(SkColor color, bool should_use_dark_colors) {
  SkColor default_color = GetDefaultProfileThemeColors(should_use_dark_colors)
                              .profile_highlight_color;
  color_utils::HSL hsl;
  color_utils::SkColorToHSL(default_color, &hsl);
  return IsLightForAutoselection(color, hsl.l);
}

class ProfileColorsUtilTest : public testing::Test {
 public:
  ProfileColorsUtilTest()
      : testing_profile_manager_(TestingBrowserProcess::GetGlobal()) {}
  ~ProfileColorsUtilTest() override = default;

 protected:
  void SetUp() override { ASSERT_TRUE(testing_profile_manager_.SetUp()); }

  ProfileAttributesEntry* AddProfile(absl::optional<SkColor> color) {
    size_t number_of_profiles = storage()->GetNumberOfProfiles();

    base::FilePath profile_path =
        testing_profile_manager_.profile_manager()->user_data_dir().AppendASCII(
            base::StringPrintf("testing_profile_path%" PRIuS,
                               number_of_profiles));
    std::u16string name = base::ASCIIToUTF16(
        base::StringPrintf("testing_profile_name%" PRIuS, number_of_profiles));
    ProfileAttributesInitParams params;
    params.profile_path = profile_path;
    params.profile_name = name;
    params.user_name = name;
    params.is_consented_primary_account = true;
    params.icon_index = number_of_profiles;
    storage()->AddProfile(std::move(params));

    EXPECT_EQ(number_of_profiles + 1, storage()->GetNumberOfProfiles());

    ProfileAttributesEntry* entry =
        storage()->GetProfileAttributesWithPath(profile_path);
    EXPECT_NE(entry, nullptr);

    if (color.has_value()) {
      entry->SetProfileThemeColors(
          GetProfileThemeColorsForAutogeneratedColor(*color));
    }

    return entry;
  }

  ProfileAttributesStorage* storage() {
    return testing_profile_manager_.profile_attributes_storage();
  }

  size_t GetAvailableColorCount(ProfileAttributesEntry* entry = nullptr) {
    size_t count;
    // Instead of providing a random number generator, return an arbitrary value
    // and capture the count of options.
    GenerateNewProfileColorWithGenerator(
        *storage(), base::BindOnce(&CaptureCountAndReturnZero, &count), entry);
    return count;
  }

  int GetAvailableColorId(size_t n, ProfileAttributesEntry* entry = nullptr) {
    // Instead of providing a random number generator, return the nth option
    // deterministically.
    return GenerateNewProfileColorWithGenerator(
               *storage(), base::BindOnce(&ReturnNth, n), entry)
        .id;
  }

  std::set<int> GetAvailableColorsIds(ProfileAttributesEntry* entry = nullptr) {
    size_t count = GetAvailableColorCount(entry);
    std::set<int> colors;
    for (size_t i = 0; i < count; i++)
      colors.insert(GetAvailableColorId(i, entry));
    return colors;
  }

  void ExpectAllSaturatedColorsAvailable(
      ProfileAttributesEntry* entry = nullptr) {
    std::set<int> available_colors = GetAvailableColorsIds(entry);
    for (size_t i = 0; i < kColorsCount; ++i) {
      if (IsSaturatedForAutoselection(GetHighlightColor(i))) {
        EXPECT_TRUE(base::Contains(available_colors, GetColor(i).id));
      } else {
        EXPECT_FALSE(base::Contains(available_colors, GetColor(i).id));
      }
    }
  }

 private:
  TestingProfileManager testing_profile_manager_;
  base::test::TaskEnvironment task_environment_;
};

TEST_F(ProfileColorsUtilTest, IsSaturatedForAutoselection) {
  // Just a sanity check. Don't want to include exact thresholds here.
  EXPECT_FALSE(IsSaturatedForAutoselection(SK_ColorBLACK));
  EXPECT_FALSE(IsSaturatedForAutoselection(SK_ColorGRAY));
  EXPECT_FALSE(IsSaturatedForAutoselection(SK_ColorWHITE));
  EXPECT_TRUE(IsSaturatedForAutoselection(SK_ColorRED));
  EXPECT_TRUE(IsSaturatedForAutoselection(SK_ColorGREEN));
  EXPECT_TRUE(IsSaturatedForAutoselection(SK_ColorBLUE));

  // Even with some transparency, it is still saturated enough.
  EXPECT_TRUE(IsSaturatedForAutoselection(SkColorSetA(SK_ColorRED, 150u)));
}

TEST_F(ProfileColorsUtilTest, IsLightForAutoselection) {
  // Just a sanity check. Don't want to include exact thresholds here.
  // Get two variants of red color: put slightly transparent red (a) on white
  // and (b) on black. These should have enough similar lightness.
  SkColor lighter = color_utils::GetResultingPaintColor(
      /*fg=*/SkColorSetA(SK_ColorRED, 200u),
      /*bg=*/SK_ColorWHITE);
  color_utils::HSL lighter_hsl;
  color_utils::SkColorToHSL(lighter, &lighter_hsl);

  SkColor darker = color_utils::GetResultingPaintColor(
      /*fg=*/SkColorSetA(SK_ColorRED, 200u),
      /*bg=*/SK_ColorBLACK);
  EXPECT_TRUE(IsLightForAutoselection(darker, lighter_hsl.l));

  // Repeat the same with more difference to get the opposite outcome.
  SkColor very_light = color_utils::GetResultingPaintColor(
      /*fg=*/SkColorSetA(SK_ColorRED, 100u),
      /*bg=*/SK_ColorWHITE);
  color_utils::HSL very_light_hsl;
  color_utils::SkColorToHSL(very_light, &very_light_hsl);

  SkColor very_dark = color_utils::GetResultingPaintColor(
      /*fg=*/SkColorSetA(SK_ColorRED, 100u),
      /*bg=*/SK_ColorBLACK);
  EXPECT_FALSE(IsLightForAutoselection(very_dark, very_light_hsl.l));
}

class ProfileColorsUtilTestDarkModeParam
    : public ProfileColorsUtilTest,
      public testing::WithParamInterface<bool> {
 public:
  void ExpectAllSaturatedColorsMatchingColorSchemeAvailable(
      bool should_use_dark_colors) {
    std::set<int> available_colors = GetAvailableColorsIds();
    for (size_t i = 0; i < kColorsCount; ++i) {
      SkColor highlight_color = GetHighlightColor(i);
      if (IsSaturatedForAutoselection(highlight_color) &&
          IsColorMatchingColorScheme(highlight_color, should_use_dark_colors)) {
        EXPECT_TRUE(base::Contains(available_colors, GetColor(i).id));
      } else {
        EXPECT_FALSE(base::Contains(available_colors, GetColor(i).id));
      }
    }
  }
};

// Test that all colors matching the native light-or-dark color scheme are
// available with no other profiles.
TEST_P(ProfileColorsUtilTestDarkModeParam,
       GenerateNewProfileColorWithNoColoredProfile) {
  bool should_use_dark_colors = GetParam();
  ui::NativeTheme::GetInstanceForNativeUi()->set_use_dark_colors(
      should_use_dark_colors);
  ExpectAllSaturatedColorsMatchingColorSchemeAvailable(should_use_dark_colors);

  // Add some profiles with the default theme.
  AddProfile(absl::nullopt);
  AddProfile(absl::nullopt);
  // Add a profile with a custom color.
  AddProfile(SK_ColorRED);

  // It still behaves the same, all colors are available.
  ExpectAllSaturatedColorsMatchingColorSchemeAvailable(should_use_dark_colors);
}

INSTANTIATE_TEST_SUITE_P(All,
                         ProfileColorsUtilTestDarkModeParam,
                         testing::Bool());

// Test that the taken colors are not available.
TEST_F(ProfileColorsUtilTest,
       GenerateNewProfileColorWithMultipleColoredProfiles) {
  std::set<int> colors_ids = GetAvailableColorsIds();
  EXPECT_TRUE(base::Contains(colors_ids, GetColor(5).id));
  EXPECT_TRUE(base::Contains(colors_ids, GetColor(6).id));
  AddProfile(GetColor(5).color);
  AddProfile(GetColor(6).color);

  std::set<int> limited_colors_ids = GetAvailableColorsIds();
  EXPECT_EQ(colors_ids.size(), limited_colors_ids.size() + 2);
  EXPECT_FALSE(base::Contains(limited_colors_ids, GetColor(5).id));
  EXPECT_FALSE(base::Contains(limited_colors_ids, GetColor(6).id));
}

// Test that specifying a profile restricts the choice to colors of similar
// lightness.
TEST_F(ProfileColorsUtilTest, GenerateNewProfileColorForCurrentProfile) {
  const size_t kCurrentProfile = 5;
  ProfileAttributesEntry* entry = AddProfile(GetColor(kCurrentProfile).color);
  color_utils::HSL current_profile_hsl;
  color_utils::SkColorToHSL(GetHighlightColor(kCurrentProfile),
                            &current_profile_hsl);

  std::set<int> colors_ids = GetAvailableColorsIds(entry);
  EXPECT_FALSE(base::Contains(colors_ids, GetColor(5).id));
  EXPECT_FALSE(colors_ids.empty());
  for (size_t i = 0; i < kColorsCount; i++) {
    const SkColor highlight_color = GetHighlightColor(i);
    if (i != kCurrentProfile && IsSaturatedForAutoselection(highlight_color) &&
        IsLightForAutoselection(highlight_color, current_profile_hsl.l)) {
      EXPECT_TRUE(base::Contains(colors_ids, GetColor(i).id));
    } else {
      EXPECT_FALSE(base::Contains(colors_ids, GetColor(i).id));
    }
  }
}

// Test that if all colors are taken, then again all are available.
TEST_F(ProfileColorsUtilTest, GenerateNewProfileColorWithAllColorsTaken) {
  for (size_t i = 0; i < kColorsCount - 1; i++)
    AddProfile(GetColor(i).color);

  // Only the last color is available.
  EXPECT_EQ(GetAvailableColorId(0), GetColor(kColorsCount - 1).id);

  // Take the last available color.
  AddProfile(GetColor(kColorsCount - 1).color);

  // Again, all colors are available.
  ExpectAllSaturatedColorsAvailable();
}

// Test that when all colors with similar lightness are taken, all saturated
// colors are available.
TEST_F(ProfileColorsUtilTest,
       GenerateNewProfileColorForCurrentProfileWithAllSimilarColorsTaken) {
  // Start with an arbitrary profile.
  const size_t kCurrentProfile = 5;
  ProfileAttributesEntry* entry = AddProfile(GetColor(kCurrentProfile).color);

  // Add profiles for almost all available colors.
  std::set<int> colors_ids = GetAvailableColorsIds(entry);
  // First keep one remaining color id without a profile.
  ASSERT_FALSE(colors_ids.empty());
  int remaining_id = *colors_ids.begin();
  SkColor remaining_color;
  for (size_t i = 0; i < kColorsCount; i++) {
    if (base::Contains(colors_ids, GetColor(i).id)) {
      if (GetColor(i).id == remaining_id) {
        remaining_color = GetColor(i).color;
      } else {
        AddProfile(GetColor(i).color);
      }
    }
  }

  // One `remaining_id` is left.
  std::set<int> singleton_colors_ids = GetAvailableColorsIds(entry);
  EXPECT_EQ(singleton_colors_ids.size(), 1u);
  EXPECT_TRUE(base::Contains(singleton_colors_ids, remaining_id));

  // Add this remaining profile, all unused colors are again available.
  AddProfile(remaining_color);
  std::set<int> remaining_ids = GetAvailableColorsIds(entry);
  for (size_t i = 0; i < kColorsCount; ++i) {
    if (IsSaturatedForAutoselection(GetHighlightColor(i)) &&
        !base::Contains(colors_ids, GetColor(i).id) && i != kCurrentProfile) {
      EXPECT_TRUE(base::Contains(remaining_ids, GetColor(i).id));
    } else {
      EXPECT_FALSE(base::Contains(remaining_ids, GetColor(i).id));
    }
  }
}

}  // namespace
